/**
 * Copyright (c) 2013-2020 Contributors to the Eclipse Foundation
 *
 * <p> See the NOTICE file distributed with this work for additional information regarding copyright
 * ownership. All rights reserved. This program and the accompanying materials are made available
 * under the terms of the Apache License, Version 2.0 which accompanies this distribution and is
 * available at http://www.apache.org/licenses/LICENSE-2.0.txt
 */
package org.locationtech.geowave.adapter.vector.plugin;

import java.awt.RenderingHints.Key;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFactorySpi;
import org.geotools.util.factory.FactoryIteratorProvider;
import org.geotools.util.factory.GeoTools;
import org.locationtech.geowave.core.store.GeoWaveStoreFinder;
import org.locationtech.geowave.core.store.StoreFactoryFamilySpi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.collect.Iterators;

/**
 * This factory is injected by GeoTools using Java SPI and is used to expose GeoWave as a DataStore
 * to GeoTools. It should be defined within a file
 * META-INF/services/org.geotools.data.DataStoreFactorySpi to inject this into GeoTools.
 */
public class GeoWaveGTDataStoreFactory implements DataStoreFactorySpi {
  private static class DataStoreCacheEntry {
    private final Map<String, ?> params;
    private final DataStore dataStore;

    public DataStoreCacheEntry(final Map<String, ?> params, final DataStore dataStore) {
      this.params = params;
      this.dataStore = dataStore;
    }
  }

  public static final String DISPLAY_NAME_PREFIX = "GeoWave Datastore - ";
  private static final Logger LOGGER = LoggerFactory.getLogger(GeoWaveGTDataStoreFactory.class);
  private final List<DataStoreCacheEntry> dataStoreCache = new ArrayList<>();
  private final StoreFactoryFamilySpi geowaveStoreFactoryFamily;
  private static Boolean isAvailable = null;

  /**
   * Public "no argument" constructor called by Factory Service Provider (SPI) entry listed in
   * META-INF/services/org.geotools.data.DataStoreFactorySPI
   */
  public GeoWaveGTDataStoreFactory() {
    final Collection<StoreFactoryFamilySpi> dataStoreFactories =
        GeoWaveStoreFinder.getRegisteredStoreFactoryFamilies().values();

    if (dataStoreFactories.isEmpty()) {
      LOGGER.error("No GeoWave DataStore found!  Geotools datastore for GeoWave is unavailable");
      geowaveStoreFactoryFamily = null;
    } else {
      final Iterator<StoreFactoryFamilySpi> it = dataStoreFactories.iterator();
      geowaveStoreFactoryFamily = it.next();
      if (it.hasNext()) {
        GeoTools.addFactoryIteratorProvider(new GeoWaveGTDataStoreFactoryIteratorProvider());
      }
    }
  }

  public GeoWaveGTDataStoreFactory(final StoreFactoryFamilySpi geowaveStoreFactoryFamily) {
    this.geowaveStoreFactoryFamily = geowaveStoreFactoryFamily;
  }

  // GeoServer seems to call this several times so we should cache a
  // connection if the parameters are the same, I'm not sure this is entirely
  // correct but it keeps us from making several connections for the same data
  // store
  @Override
  public DataStore createDataStore(final Map<String, ?> params) throws IOException {
    // iterate in reverse over the cache so the most recently added is
    // accessed first
    for (int index = dataStoreCache.size() - 1; index >= 0; index--) {
      final DataStoreCacheEntry cacheEntry = dataStoreCache.get(index);
      if (paramsEqual(params, cacheEntry.params)) {
        return cacheEntry.dataStore;
      }
    }
    return createNewDataStore(params);
  }

  private boolean paramsEqual(final Map<String, ?> params1, final Map<String, ?> params2) {
    if (params1.size() == params2.size()) {
      for (final Entry<String, ?> entry : params1.entrySet()) {
        final Object value = params2.get(entry.getKey());
        if (value == null) {
          if (entry.getValue() == null) {
            continue;
          }
          return false;
        } else if (!value.equals(entry.getValue())) {
          return false;
        }
      }
      return true;
    }
    return false;
  }

  @Override
  public DataStore createNewDataStore(final Map<String, ?> params) throws IOException {
    final GeoWaveGTDataStore dataStore;
    try {
      dataStore =
          new GeoWaveGTDataStore(new GeoWavePluginConfig(geowaveStoreFactoryFamily, params));
      dataStoreCache.add(new DataStoreCacheEntry(params, dataStore));
    } catch (final Exception ex) {
      throw new IOException("Error initializing datastore", ex);
    }
    return dataStore;
  }

  @Override
  public String getDisplayName() {
    return DISPLAY_NAME_PREFIX + geowaveStoreFactoryFamily.getType().toUpperCase();
  }

  @Override
  public String getDescription() {
    return "A datastore that uses the GeoWave API for spatial data persistence in "
        + geowaveStoreFactoryFamily.getType()
        + ". "
        + geowaveStoreFactoryFamily.getDescription();
  }

  @Override
  public Param[] getParametersInfo() {
    final List<Param> params = GeoWavePluginConfig.getPluginParams(geowaveStoreFactoryFamily);
    return params.toArray(new Param[params.size()]);
  }

  @Override
  public boolean canProcess(final Map<String, ?> params) {
    try {
      final Map<String, String> dataStoreParams =
          params.entrySet().stream().filter(
              e -> !GeoWavePluginConfig.BASE_GEOWAVE_PLUGIN_PARAM_KEYS.contains(
                  e.getKey())).collect(
                      HashMap::new,
                      (m, e) -> m.put(
                          e.getKey() == null ? null : e.getKey().toString(),
                          e.getValue() == null ? null : e.getValue().toString()),
                      HashMap::putAll);

      final Map<String, String> originalParams =
          params.entrySet().stream().collect(
              HashMap::new,
              (m, e) -> m.put(
                  e.getKey() == null ? null : e.getKey().toString(),
                  e.getValue() == null ? null : e.getValue().toString()),
              HashMap::putAll);
      return GeoWaveStoreFinder.exactMatch(
          geowaveStoreFactoryFamily,
          dataStoreParams,
          originalParams);
    } catch (final Exception e) {
      LOGGER.info("unable to process params as GeoWave datastore", e);
      return false;
    }
  }

  @Override
  public synchronized boolean isAvailable() {
    if (isAvailable == null) {
      if (geowaveStoreFactoryFamily == null) {
        isAvailable = false;
      } else {
        try {
          Class.forName("org.locationtech.geowave.adapter.vector.plugin.GeoWaveGTDataStore");
          isAvailable = true;
        } catch (final ClassNotFoundException e) {
          isAvailable = false;
        }
      }
    }
    return isAvailable;
  }

  @Override
  public Map<Key, ?> getImplementationHints() {
    // No implementation hints required at this time
    return Collections.emptyMap();
  }

  private static class GeoWaveGTDataStoreFactoryIteratorProvider implements
      FactoryIteratorProvider {

    @Override
    public <T> Iterator<T> iterator(final Class<T> cls) {
      if ((cls != null) && cls.isAssignableFrom(DataStoreFactorySpi.class)) {
        return (Iterator<T>) new GeoWaveGTDataStoreFactoryIterator();
      }
      return null;
    }

    private static class GeoWaveGTDataStoreFactoryIterator implements
        Iterator<DataStoreFactorySpi> {
      private final Iterator<DataStoreFactorySpi> it;

      private GeoWaveGTDataStoreFactoryIterator() {
        final Iterator<StoreFactoryFamilySpi> geowaveDataStoreIt =
            GeoWaveStoreFinder.getRegisteredStoreFactoryFamilies().values().iterator();
        geowaveDataStoreIt.next();
        it = Iterators.transform(geowaveDataStoreIt, new GeoWaveStoreToGeoToolsDataStore());
      }

      @Override
      public boolean hasNext() {
        return it.hasNext();
      }

      @Override
      public DataStoreFactorySpi next() {
        return it.next();
      }

      @Override
      public void remove() {}
    }
  }

  /**
   * Below is a set of 9 additional GeoWaveGTDataStoreFactory's, its a bit of a hack, but must be
   * done because the geotools factory registry will re-use instances of the same class, so each
   * individual geowave data store must be registered as a different class (the alternative is
   * dynamic compilation of classes to add to the classloader).
   */
  private static class GeoWaveStoreToGeoToolsDataStore implements
      Function<StoreFactoryFamilySpi, DataStoreFactorySpi> {
    private int i = 0;

    public GeoWaveStoreToGeoToolsDataStore() {}

    @Override
    public DataStoreFactorySpi apply(final StoreFactoryFamilySpi input) {
      i++;
      switch (i) {
        case 1:
          return new GeoWaveGTDataStoreFactory1(input);
        case 2:
          return new GeoWaveGTDataStoreFactory2(input);
        case 3:
          return new GeoWaveGTDataStoreFactory3(input);
        case 4:
          return new GeoWaveGTDataStoreFactory4(input);
        case 5:
          return new GeoWaveGTDataStoreFactory5(input);
        case 6:
          return new GeoWaveGTDataStoreFactory6(input);
        case 7:
          return new GeoWaveGTDataStoreFactory7(input);
        case 8:
          return new GeoWaveGTDataStoreFactory8(input);
        case 9:
          return new GeoWaveGTDataStoreFactory9(input);
      }
      LOGGER.error("Too many GeoWave Datastores registered for GeoTools data store");
      return new GeoWaveGTDataStoreFactory(input);
    }
  }

  private static class GeoWaveGTDataStoreFactory1 extends GeoWaveGTDataStoreFactory {

    public GeoWaveGTDataStoreFactory1(final StoreFactoryFamilySpi geowaveStoreFactoryFamily) {
      super(geowaveStoreFactoryFamily);
    }
  }

  private static class GeoWaveGTDataStoreFactory2 extends GeoWaveGTDataStoreFactory {

    public GeoWaveGTDataStoreFactory2(final StoreFactoryFamilySpi geowaveStoreFactoryFamily) {
      super(geowaveStoreFactoryFamily);
    }
  }

  private static class GeoWaveGTDataStoreFactory3 extends GeoWaveGTDataStoreFactory {

    public GeoWaveGTDataStoreFactory3(final StoreFactoryFamilySpi geowaveStoreFactoryFamily) {
      super(geowaveStoreFactoryFamily);
    }
  }

  private static class GeoWaveGTDataStoreFactory4 extends GeoWaveGTDataStoreFactory {

    public GeoWaveGTDataStoreFactory4(final StoreFactoryFamilySpi geowaveStoreFactoryFamily) {
      super(geowaveStoreFactoryFamily);
    }
  }

  private static class GeoWaveGTDataStoreFactory5 extends GeoWaveGTDataStoreFactory {

    public GeoWaveGTDataStoreFactory5(final StoreFactoryFamilySpi geowaveStoreFactoryFamily) {
      super(geowaveStoreFactoryFamily);
    }
  }

  private static class GeoWaveGTDataStoreFactory6 extends GeoWaveGTDataStoreFactory {

    public GeoWaveGTDataStoreFactory6(final StoreFactoryFamilySpi geowaveStoreFactoryFamily) {
      super(geowaveStoreFactoryFamily);
    }
  }

  private static class GeoWaveGTDataStoreFactory7 extends GeoWaveGTDataStoreFactory {

    public GeoWaveGTDataStoreFactory7(final StoreFactoryFamilySpi geowaveStoreFactoryFamily) {
      super(geowaveStoreFactoryFamily);
    }
  }

  private static class GeoWaveGTDataStoreFactory8 extends GeoWaveGTDataStoreFactory {

    public GeoWaveGTDataStoreFactory8(final StoreFactoryFamilySpi geowaveStoreFactoryFamily) {
      super(geowaveStoreFactoryFamily);
    }
  }

  private static class GeoWaveGTDataStoreFactory9 extends GeoWaveGTDataStoreFactory {

    public GeoWaveGTDataStoreFactory9(final StoreFactoryFamilySpi geowaveStoreFactoryFamily) {
      super(geowaveStoreFactoryFamily);
    }
  }
}
